﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using RayTracer.Mathematics;
using System.Xml;
using System.IO;
using RayTracer;

namespace GPURayTracer
{
    /// <summary>
    /// Kapselt alle Userdefinded Anteile.
    /// </summary>
    public partial class RayTracerControl : UserControl
    {
        #region Typen
        /// <summary>
        /// Ein Delegat für einfache Ereignisse des Controls.
        /// </summary>
        /// <param name="sender">Das Sendercontrol.</param>
        public delegate void SimpleEvent(RayTracerControl sender);

        /// <summary>
        /// Summe der möglichen Interaktionstypen.
        /// </summary>
        public enum InteractionType
        {
            /// <summary>
            /// Gibt an dass Rotiert wird.
            /// </summary>
            Rotation,

            /// <summary>
            /// Gibt an dass die Position verändert wird.
            /// </summary>
            Position
        }
        #endregion Typen

        #region Attribute
        private Point oldMousePosition;
        private InteractionType interactionType = InteractionType.Rotation;
        private XmlDocument sceneXml;
        private bool mouseCatched = false;
        private bool showHelp = true;
        private bool showHelpHint = true;
        private static ComponentResourceManager resources = new ComponentResourceManager(typeof(RayTracerControl));
        private static Cursor cursorPosition = new Cursor(new MemoryStream((byte[])RayTracerControl.resources.GetObject("$this.PositionCursor"), false));
        private static Cursor cursorRotation = new Cursor(new MemoryStream((byte[])RayTracerControl.resources.GetObject("$this.RotationCursor"), false));
        #endregion Attribute

        #region Eigenschaften
        /// <summary>
        /// Gibt und setzt den verwendeten RayTracer.
        /// </summary>
        public global::RayTracer.RayTracer RayTracer
        {
            get { return vpRayTracer.RayTracer; }
            set { vpRayTracer.RayTracer = value; if (value != null) Scene(value); }
        }

        /// <summary>
        /// Wird ausgelöst wenn die Einstellungen aufgerufen werden sollen.
        /// </summary>
        public event SimpleEvent SettingsCall;

        /// <summary>
        /// Wird ausgelöst wenn die Anwendung beendet werden soll.
        /// </summary>
        public event SimpleEvent ExitCall;

        /// <summary>
        /// Gibt und setzt den Indikator ob die Hilfe angezeigt werden soll.
        /// </summary>
        public bool ShowHelp { get { return showHelp; } set { showHelp = value; } }

        /// <summary>
        /// Gibt und setzt den Indikator ob der Hilfehinweis angezeigt werden soll.
        /// </summary>
        public bool ShowHelpHint { get { return showHelpHint; } set { showHelpHint = value; } }

        /// <summary>
        /// Gibt und setzt die Szenen-XML-Daten
        /// </summary>
        public XmlDocument SceneXml
        {
            get { return sceneXml; }
            set
            {
                sceneXml = value;
                if (RayTracer != null) Scene(RayTracer);
            }
        }

        /// <summary>
        /// Gibt und setzt den Interaktionstypen.
        /// </summary>
        public InteractionType Interaction { get { return interactionType; } set { interactionType = value; } }
        #endregion Eigenschaften

        #region Methoden
        /// <summary>
        /// Instanziiert und initialisiert das Objekt.
        /// </summary>
        public RayTracerControl()
        {
            InitializeComponent();

            vpRayTracer.MouseWheel += new MouseEventHandler(vpRayTracer_MouseWheel);
            this.MouseWheel += new MouseEventHandler(vpRayTracer_MouseWheel);
        }

        /// <summary>
        /// Die Szene.
        /// </summary>
        /// <param name="rt">Der RayTracer.</param>
        public void Scene(RayTracer.RayTracer rt)
        {
            if (sceneXml == null)
            {
                // Lichter
                rt.Scene.Add(new RayTracer.Environment.Lights.SimpleLight(new RayTracer.Structures.Point(0, 5, 0)));
                rt.Scene.Add(new RayTracer.Environment.Lights.SimpleLight(new RayTracer.Structures.Point(0, -5, 0)));

                // Kugel 1
                RayTracer.Environment.Objects.Sphere s1 = new RayTracer.Environment.Objects.Sphere(new RayTracer.Structures.Point(-1, 0, 0));
                s1.Material = global::RayTracer.Environment.Materials.Transparent.Glas;
                rt.Scene.Add(s1);

                // Kugel 2
                RayTracer.Environment.Objects.Sphere s2 = new RayTracer.Environment.Objects.Sphere(new RayTracer.Structures.Point(1, 0, 0));
                s2.Material = global::RayTracer.Environment.Materials.Transparent.Rubin;
                rt.Scene.Add(s2);

                // Kamera
                rt.Scene.Camera.LookAt(new RayTracer.Structures.Point(0, 0, -10),
                                       new RayTracer.Structures.Point(0, 0, 0),
                                       new RayTracer.Structures.Vector(0, 1, 0),
                                       25);
            }
            else
            {
                rt.Scene.Clear();
                rt.Scene.AppendXml(sceneXml);
            }

            rt.UpdateScene();
        }

        /// <summary>
        /// Zeigt die zusätzliche HUD an.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void vpRayTracer_Paint(object sender, PaintEventArgs e)
        {
            if (showHelp)
            {
                e.Graphics.WriteHUDText(new PointF(5, vpRayTracer.Height - 20), "linke Maustaste", Color.White, Color.Black);
                e.Graphics.WriteHUDText(new PointF(140, vpRayTracer.Height - 20), "zum Interagieren drücken.", Color.Gold, Color.Black);

                e.Graphics.WriteHUDText(new PointF(5, vpRayTracer.Height - 40), "rechte Maustaste", Color.White, Color.Black);
                e.Graphics.WriteHUDText(new PointF(149, vpRayTracer.Height - 40), "drücken um das Menü zu öffnen.", Color.Gold, Color.Black);

                e.Graphics.WriteHUDText(new PointF(5, vpRayTracer.Height - 60), "Mausrad ", Color.White, Color.Black);
                e.Graphics.WriteHUDText(new PointF(77, vpRayTracer.Height - 60), "drehen zum Zoomen.", Color.Gold, Color.Black);

                if (showHelpHint)
                {
                    e.Graphics.WriteHUDText(new PointF(5, vpRayTracer.Height - 80), "F1 ", Color.Gray, Color.Black);
                    e.Graphics.WriteHUDText(new PointF(31, vpRayTracer.Height - 80), "um Hilfe auszublenden.", Color.DarkGoldenrod, Color.Black);
                }
            }
            else if (showHelpHint)
            {
                e.Graphics.WriteHUDText(new PointF(5, vpRayTracer.Height - 20), "F1 ", Color.FromArgb(32, 128, 128, 128), Color.FromArgb(32, 0, 0, 0));
                e.Graphics.WriteHUDText(new PointF(31, vpRayTracer.Height - 20), "um Hilfe einzublenden.", Color.FromArgb(32, 255, 215, 0), Color.FromArgb(32, 0, 0, 0));
            }
        }

        /// <summary>
        /// Erstellt einen Screenshot.
        /// </summary>
        private void screenshot()
        {
            try
            {
                Bitmap img = vpRayTracer.CurrentPicture;
                SaveFileDialog sfd = new SaveFileDialog();
                sfd.DefaultExt = "jpg";
                sfd.CheckPathExists = true;
                sfd.CreatePrompt = false;
                sfd.OverwritePrompt = true;
                sfd.AddExtension = true;
                sfd.Filter = "JPEG-Datei (*.jpg)|*.jpg|PNG-Datei (*.png)|*.png|BMP-Datei (*.bmp)|*.bmp";
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    switch (Path.GetExtension(sfd.FileName).ToLower())
                    {
                        case ".jpg":
                        case ".jpeg":
                            img.Save(sfd.FileName, System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                        case ".bmp":
                            img.Save(sfd.FileName, System.Drawing.Imaging.ImageFormat.Bmp);
                            break;
                        case ".png":
                            img.Save(sfd.FileName, System.Drawing.Imaging.ImageFormat.Png);
                            break;
                        default:
                            img.Save(sfd.FileName + ".jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
                            break;
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(this, "Das Speichern des Bildes ist fehlgeschlagen.\n\nHinweis: " + ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        #region Eingabe
        #region Tastatur
        /// <summary>
        /// Behandelt Tastatureingaben.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void RayTracerControl_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.KeyData == Keys.F12)
            {
                e.Handled = true;
                if (SettingsCall != null) SettingsCall(this);
            }
            else if (e.KeyData == Keys.Escape)
            {
                e.Handled = true;
                if (ExitCall != null) ExitCall(this);
            }
            else if (e.KeyData == Keys.F1)
            {
                e.Handled = true;
                showHelp = !showHelp;
            }
            else if (e.KeyData == Keys.PrintScreen)
            {
                e.Handled = true;
                screenshot();
            }
        }
        #endregion Tastatur
        #region Maus
        #region Mausinteraktion mit der Kamera
        private object interactionLock = new object();
        /// <summary>
        /// Setzt die Mausbewegung in eine Kamerabewegung um.
        /// </summary>
        /// <param name="p"></param>
        private void handleMouseMovement(Point p)
        {
            if (interactionType == InteractionType.Rotation)
            {
                lock (interactionLock)
                {
                    if (p.IsEmpty) return;
                    vpRayTracer.Refresh();

                    RayTracer.Structures.Point pos = Program.RayTracer.Scene.Camera.Position;
                    pos.Rotate(new RayTracer.Structures.Vector(0, 1, 0), 1.0f * -p.X);
                    pos.Rotate(new RayTracer.Structures.Vector(1, 0, 0), 1.0f * -p.Y);

                    Program.RayTracer.Scene.Camera.LookAt(
                                pos,
                                Program.RayTracer.Scene.Camera.LookAtPosition,
                                Program.RayTracer.Scene.Camera.TopDirection);
                }
            }
            else if (interactionType == InteractionType.Position)
            {
                lock (interactionLock)
                {
                    if (p.IsEmpty) return;
                    vpRayTracer.Refresh();

                    Program.RayTracer.Scene.Camera.LookAt(
                        new RayTracer.Structures.Point(
                               Program.RayTracer.Scene.Camera.Position.X + (p.X / (float)Math.Max(Size.Width, 1)) * 10.0f,
                               Program.RayTracer.Scene.Camera.Position.Y - (p.Y / (float)Math.Max(Size.Height, 1)) * 10.0f,
                               Program.RayTracer.Scene.Camera.Position.Z
                            ),
                            Program.RayTracer.Scene.Camera.LookAtPosition,
                            Program.RayTracer.Scene.Camera.TopDirection);
                }
            }
        }

        /// <summary>
        /// Behandelt das Mausrad und zoomt entsprechend.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        void vpRayTracer_MouseWheel(object sender, MouseEventArgs e)
        {
            if (e.Delta != 0)
            {
                showHelp = false;
                lock (interactionLock)
                {
                    global::RayTracer.Structures.Vector v = Program.RayTracer.Scene.Camera.Position - Program.RayTracer.Scene.Camera.LookAtPosition;
                    v.Length = v.Length * (1 - 0.1f * (e.Delta / (float)120));

                    Program.RayTracer.Scene.Camera.LookAt(
                         v - Program.RayTracer.Scene.Camera.LookAtPosition,
                         Program.RayTracer.Scene.Camera.LookAtPosition,
                         Program.RayTracer.Scene.Camera.TopDirection);
                }
            }
        }

        /// <summary>
        /// Verfolgt die Mausbewegung.
        /// </summary>
        private void trackMouse()
        {
            Point diff;
            lock (interactionLock)
            {
                diff = new Point(Cursor.Position.X - vpRayTracer.CenterScreenPoint.X, Cursor.Position.Y - vpRayTracer.CenterScreenPoint.Y);
                Cursor.Position = vpRayTracer.CenterScreenPoint;
            }
            handleMouseMovement(diff);
        }

        /// <summary>
        /// Verbirgt den Mauszeiger sobald die linke Maustaste gedrückt ist.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void vpRayTracer_MouseDown(object sender, MouseEventArgs e)
        {
            showHelp = false;
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                lock (interactionLock) { oldMousePosition = Cursor.Position; }

                if (interactionType == InteractionType.Position) this.vpRayTracer.Cursor = cursorPosition;
                else if (interactionType == InteractionType.Rotation) this.vpRayTracer.Cursor = cursorRotation;

                //this.vpRayTracer.Cursor = new Cursor(typeof(RayTracerControl), "$this.PositionCursor");
                lock (interactionLock) { Cursor.Position = vpRayTracer.CenterScreenPoint; }
                mouseCatched = true;
            }
        }

        /// <summary>
        /// Trackt die Maus wenn die linke Maustaste gedrückt ist.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void vpRayTracer_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left) trackMouse();
        }

        /// <summary>
        /// Setzt den Mauszeiger an seine Ursprungsposition zurück, wenn die linke Maustaste losgelassen wurde.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void vpRayTracer_MouseUp(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                if (mouseCatched) lock (interactionLock) { Cursor.Position = oldMousePosition; }
                this.vpRayTracer.Cursor = Cursors.Default;
                mouseCatched = false;
            }
        }
        #endregion Mausinteraktion mit der Kamera
        #region Kontextmenü
        /// <summary>
        /// Wechselt die Anzeige der FPS.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiFPS_Click(object sender, EventArgs e)
        {
            vpRayTracer.ShowFPS = !vpRayTracer.ShowFPS;
        }

        /// <summary>
        /// Ändert die Beschriftung/den Status adäquat.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiFPS_Paint(object sender, PaintEventArgs e)
        {
            tsmiFPS.Checked = vpRayTracer.ShowFPS;
        }

        /// <summary>
        /// Wechselt die Anzeige der Hilfehinweisanzeige. (Die Anzeige für den Hinweis auf F1.)
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiHelpHint_Click(object sender, EventArgs e)
        {
            showHelpHint = !showHelpHint;
        }

        /// <summary>
        /// Ändert die Beschriftung/den Status adäquat.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiHelpHint_Paint(object sender, PaintEventArgs e)
        {
            tsmiHelpHint.Checked = showHelpHint;
        }

        /// <summary>
        /// Öffnet die Renderereinstellungen.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiRenderer_Click(object sender, EventArgs e)
        {
            if (SettingsCall != null) SettingsCall(this);
        }

        /// <summary>
        /// Beendet die Anwendung.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiExit_Click(object sender, EventArgs e)
        {
            if (ExitCall != null) ExitCall(this);
        }

        /// <summary>
        /// Setzt den Positionsinteraktionsmodus.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>      
        private void tsmiInteractPosition_Click(object sender, EventArgs e)
        {
            interactionType = InteractionType.Position;
            tsmiInteractRotation.Checked = false;
            tsmiInteractPosition.CheckState = CheckState.Indeterminate;
        }

        /// <summary>
        /// Setzt den Rotationsinteraktionsmodus.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void tsmiInteractRotation_Click(object sender, EventArgs e)
        {
            interactionType = InteractionType.Rotation;
            tsmiInteractPosition.Checked = false;
            tsmiInteractRotation.CheckState = CheckState.Indeterminate;
        }

        /// <summary>
        /// Lädt eine Szene.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void ladenToolStripMenuItem_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.DefaultExt = "ray";
            ofd.Filter = "ray-Dateien (*.ray)|*.ray|Alle Dateien (*.*)|*.*";
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                if (File.Exists(ofd.FileName))
                {
                    try
                    {
                        sceneXml = new XmlDocument();
                        sceneXml.Load(ofd.FileName);
                        Program.RayTracer.Scene.Clear();
                        Program.RayTracer.Scene.AppendXml(sceneXml);
                        Program.RayTracer.UpdateScene();
                    }
                    catch (Exception ex)
                    {
                        sceneXml = null;
                        MessageBox.Show(this, "Die Szene konnte nicht geladen werden.\n\nMeldung: " + ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }
            }
        }

        /// <summary>
        /// Speichert die aktuelle Szene und die Szeneneinstellungen.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void speichernToolStripMenuItem_Click(object sender, EventArgs e)
        {
            SaveFileDialog sfd = new SaveFileDialog();
            sfd.DefaultExt = "ray";
            sfd.Filter = "ray-Dateien (*.ray)|*.ray";
            sfd.CheckPathExists = true;
            sfd.CreatePrompt = false;
            sfd.OverwritePrompt = true;
            if (sfd.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    sceneXml = Program.RayTracer.Scene.ToXml();
                    sceneXml.Save(sfd.FileName);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(this, "Die Szene konnte nicht gespeichert werden.\n\nMeldung: " + ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        /// <summary>
        /// Erstellt ein Screenshot des aktuellen Bilds.
        /// </summary>
        /// <param name="sender">Der Sender.</param>
        /// <param name="e">Die Ereignisargumente.</param>
        private void screenshotToolStripMenuItem_Click(object sender, EventArgs e)
        {
            screenshot();
        }
        #endregion Kontextmenü
        #endregion Maus
        #endregion Eingabe
        #endregion Methoden
    }
}
